home *** CD-ROM | disk | FTP | other *** search
/ PC Format (PL) 2008 February / PC_Format_022008.iso / Internet / Mozilla Thunderbird wtyczki / lightning-0.7-tb-win.xpi / components / calDavCalendar.js < prev    next >
Encoding:
Text File  |  2007-09-22  |  58.7 KB  |  1,570 lines

  1. /* -*- Mode: javascript; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
  2. /* ***** BEGIN LICENSE BLOCK *****
  3.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  4.  *
  5.  * The contents of this file are subject to the Mozilla Public License Version
  6.  * 1.1 (the "License"); you may not use this file except in compliance with
  7.  * the License. You may obtain a copy of the License at
  8.  * http://www.mozilla.org/MPL/
  9.  *
  10.  * Software distributed under the License is distributed on an "AS IS" basis,
  11.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  12.  * for the specific language governing rights and limitations under the
  13.  * License.
  14.  *
  15.  * The Original Code is Oracle Corporation code.
  16.  *
  17.  * The Initial Developer of the Original Code is
  18.  *  Oracle Corporation
  19.  * Portions created by the Initial Developer are Copyright (C) 2004
  20.  * the Initial Developer. All Rights Reserved.
  21.  *
  22.  * Contributor(s):
  23.  *   Vladimir Vukicevic <vladimir.vukicevic@oracle.com>
  24.  *   Dan Mosedale <dan.mosedale@oracle.com>
  25.  *   Mike Shaver <mike.x.shaver@oracle.com>
  26.  *   Gary van der Merwe <garyvdm@gmail.com>
  27.  *   Bruno Browning <browning@uwalumni.com>
  28.  *   Matthew Willis <lilmatt@mozilla.com>
  29.  *   Daniel Boelzle <daniel.boelzle@sun.com>
  30.  *
  31.  * Alternatively, the contents of this file may be used under the terms of
  32.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  33.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  34.  * in which case the provisions of the GPL or the LGPL are applicable instead
  35.  * of those above. If you wish to allow use of your version of this file only
  36.  * under the terms of either the GPL or the LGPL, and not to allow others to
  37.  * use your version of this file under the terms of the MPL, indicate your
  38.  * decision by deleting the provisions above and replace them with the notice
  39.  * and other provisions required by the GPL or the LGPL. If you do not delete
  40.  * the provisions above, a recipient may use your version of this file under
  41.  * the terms of any one of the MPL, the GPL or the LGPL.
  42.  *
  43.  * ***** END LICENSE BLOCK ***** */
  44.  
  45. //
  46. // calDavCalendar.js
  47. //
  48.  
  49. // XXXdmose deal with generation goop
  50.  
  51. // XXXdmose need to re-query for add & modify to get up-to-date items
  52.  
  53. // XXXdmose deal with locking
  54.  
  55. // XXXdmose need to make and use better error reporting interface for webdav
  56. // (all uses of aStatusCode, probably)
  57.  
  58. // XXXdmose use real calendar result codes, not NS_ERROR_FAILURE for everything
  59.  
  60. const xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>\n';
  61.  
  62. function calDavCalendar() {
  63.     this.wrappedJSObject = this;
  64.     this.mObservers = new calListenerBag(Components.interfaces.calIObserver);
  65.     this.unmappedProperties = [];
  66.     this.mPendingStartupRequests = [];
  67.     this.mUriParams = null;
  68.     this.mEtagCache = [];
  69.     this.mDisabled = false;
  70. }
  71.  
  72. // some shorthand
  73. const nsIWebDAVOperationListener = 
  74.     Components.interfaces.nsIWebDAVOperationListener;
  75. const calICalendar = Components.interfaces.calICalendar;
  76. const nsISupportsCString = Components.interfaces.nsISupportsCString;
  77. const calIErrors = Components.interfaces.calIErrors;
  78.  
  79. var appInfo = Components.classes["@mozilla.org/xre/app-info;1"]
  80.                         .getService(Components.interfaces.nsIXULAppInfo);
  81. var isOnBranch = appInfo.platformVersion.indexOf("1.8") == 0;
  82.  
  83.  
  84. function makeOccurrence(item, start, end)
  85. {
  86.     var occ = item.createProxy();
  87.     occ.recurrenceId = start;
  88.     occ.startDate = start;
  89.     occ.endDate = end;
  90.  
  91.     return occ;
  92. }
  93.   
  94. // END_OF_TIME needs to be the max value a PRTime can be
  95. const START_OF_TIME = -0x7fffffffffffffff;
  96. const END_OF_TIME = 0x7fffffffffffffff;
  97.  
  98. // used by mAuthenticationStatus
  99. const kCaldavNoAuthentication = 0;
  100. const kCaldavFirstRequestSent = 1;      // Queueing subsequent requests
  101. const kCaldavFreshlyAuthenticated = 2;  // Need to process queue
  102. const kCaldavAuthenticated = 3;         // Queue being processed or empty
  103.  
  104. // used in checking calendar URI for (Cal)DAV-ness
  105. const kDavResourceTypeNone = 0;
  106. const kDavResourceTypeCollection = 1;
  107. const kDavResourceTypeCalendar = 2;
  108.  
  109. // used for etag checking
  110. const CALDAV_ADOPT_ITEM = 1;
  111. const CALDAV_MODIFY_ITEM = 2;
  112. const CALDAV_DELETE_ITEM = 3;
  113. const CALDAV_CACHE_ETAG = 4;
  114.  
  115. calDavCalendar.prototype = {
  116.     //
  117.     // nsISupports interface
  118.     // 
  119.     QueryInterface: function (aIID) {
  120.         if (!aIID.equals(Components.interfaces.nsISupports) &&
  121.             !aIID.equals(Components.interfaces.calICalendarProvider) &&
  122.             !aIID.equals(Components.interfaces.calICalendar) &&
  123.             !aIID.equals(Components.interfaces.nsIInterfaceRequestor)) {
  124.             throw Components.results.NS_ERROR_NO_INTERFACE;
  125.         }
  126.  
  127.         return this;
  128.     },
  129.  
  130.     //
  131.     // calICalendarProvider interface
  132.     //
  133.     get prefChromeOverlay() {
  134.         return null;
  135.     },
  136.  
  137.     get displayName() {
  138.         return calGetString("calendar", "caldavName");
  139.     },
  140.  
  141.     createCalendar: function caldav_createCal() {
  142.         throw NS_ERROR_NOT_IMPLEMENTED;
  143.     },
  144.  
  145.     deleteCalendar: function caldav_deleteCal(cal, listener) {
  146.         throw NS_ERROR_NOT_IMPLEMENTED;
  147.     },
  148.  
  149.     //
  150.     // calICalendar interface
  151.     //
  152.  
  153.     // attribute AUTF8String id;
  154.     mID: null,
  155.     get id() {
  156.         return this.mID;
  157.     },
  158.     set id(id) {
  159.         if (this.mID)
  160.             throw Components.results.NS_ERROR_ALREADY_INITIALIZED;
  161.         return (this.mID = id);
  162.     },
  163.  
  164.     // attribute AUTF8String name;
  165.     get name() {
  166.         return getCalendarManager().getCalendarPref(this, "NAME");
  167.     },
  168.     set name(name) {
  169.         getCalendarManager().setCalendarPref(this, "NAME", name);
  170.     },
  171.  
  172.     // readonly attribute AUTF8String type;
  173.     get type() { return "caldav"; },
  174.  
  175.     mReadOnly: false,
  176.  
  177.     get readOnly() { 
  178.         return this.mReadOnly;
  179.     },
  180.     set readOnly(bool) {
  181.         this.mReadOnly = bool;
  182.     },
  183.  
  184.     mDisabled: false,
  185.  
  186.     // attribute PRUInt8 mAuthenticationStatus;
  187.     mAuthenticationStatus: 0,
  188.  
  189.     mPendingStartupRequests: null,
  190.  
  191.     get canRefresh() {
  192.         return true;
  193.     },
  194.  
  195.     // mUriParams stores trailing ?parameters from the
  196.     // supplied calendar URI. Needed for (at least) Cosmo
  197.     // tickets
  198.     mUriParams: null,
  199.  
  200.     // attribute nsIURI uri;
  201.     mUri: null,
  202.     get uri() { return this.mUri; },
  203.     set uri(aUri) { return (this.mUri = aUri); },
  204.  
  205.     get mCalendarUri() { 
  206.         calUri = this.mUri.clone();
  207.         var parts = calUri.spec.split('?');
  208.         if (parts.length > 1) {
  209.             calUri.spec = parts.shift();
  210.             this.mUriParams = '?' + parts.join('?');
  211.         }
  212.         if (calUri.spec.charAt(calUri.spec.length-1) != '/') {
  213.             calUri.spec += "/";
  214.         }
  215.         return calUri;
  216.     },
  217.     
  218.     makeUri: function caldav_makeUri(aInsertString) {
  219.         var spec = this.mCalendarUri.spec + aInsertString;
  220.         if (this.mUriParams) {
  221.             return spec + this.mUriParams;
  222.         }
  223.         return spec;
  224.     },
  225.  
  226.     get mLocationPath() {
  227.         return decodeURIComponent(this.mCalendarUri.path);
  228.     },
  229.  
  230.     refresh: function caldav_refresh() {
  231.         // clear etag cache
  232.         this.mEtagCache = [];
  233.         this.mObservers.notify("onLoad", [this]);
  234.     },
  235.  
  236.     // attribute boolean suppressAlarms;
  237.     mSuppressAlarms: false,
  238.     get suppressAlarms() {
  239.         return this.mSuppressAlarms;
  240.     },
  241.     set suppressAlarms(aSuppressAlarms) {
  242.         return (this.mSuppressAlarms = aSuppressAlarms);
  243.     },
  244.  
  245.     // XXX todo: in general we want to do CalDAV scheduling, but for servers
  246.     //           that don't support it, we want Itip
  247.     get sendItipInvitations() { return true; },
  248.  
  249.     // void addObserver( in calIObserver observer );
  250.     addObserver: function caldav_addObserver(aObserver) {
  251.         this.mObservers.add(aObserver);
  252.     },
  253.  
  254.     // void removeObserver( in calIObserver observer );
  255.     removeObserver: function (aObserver) {
  256.         this.mObservers.remove(aObserver);
  257.     },
  258.  
  259.     /**
  260.      * Fetches etag from server and compares with local cached version
  261.      * before we add/adopt/modify/delete item.
  262.      *
  263.      * @param aMethod     requested method (adopt/modify/delete)
  264.      * @param aItem       item to check
  265.      * @param aListener   listener from original request
  266.      * @param aOldItem    aOldItem argument in modifyItem requests
  267.      */
  268.     fetchEtag: function caldavFE(aMethod, aItem, aListener, aOldItem) {
  269.         if (this.readOnly) {
  270.             throw calIErrors.CAL_IS_READONLY;
  271.         }
  272.  
  273.         var serverEtag = null;
  274.         var listener = new WebDavListener();
  275.  
  276.         var thisCalendar = this;
  277.  
  278.         var itemUri = this.mCalendarUri.clone();
  279.  
  280.         try {
  281.             itemUri.spec = this.makeUri(aItem.getProperty("X-MOZ-LOCATIONPATH"));
  282.             LOG("using X-MOZ-LOCATIONPATH: " + itemUri.spec);
  283.         } catch (ex) {
  284.             // XXX how are we REALLY supposed to figure this out?
  285.             itemUri.spec = this.makeUri(aItem.id + ".ics");
  286.         }
  287.  
  288.         var itemResource = new WebDavResource(itemUri);
  289.  
  290.         listener.onOperationComplete = function OOC(aStatusCode,
  291.                                                     aResource,
  292.                                                     aOperation,
  293.                                                     aClosure)
  294.         {
  295.             var mismatch = (serverEtag != thisCalendar.mEtagCache[aItem.id]);
  296.  
  297.             switch (aMethod) {
  298.                 case CALDAV_ADOPT_ITEM:
  299.                     if (serverEtag != null) {
  300.                         // The server thinks it already has an item we want to
  301.                         // create as new. This either a server error or we're
  302.                         // trying to copy an item onto itself; either way the
  303.                         // safe thing to do is abort the operation.
  304.                         LOG("CalDAV: non-null etag in adoptItem");
  305.                         thisCalendar.readOnly = true;
  306.                     } else {
  307.                         thisCalendar.performAdoptItem(aItem, aListener);
  308.                     }
  309.                     break;
  310.  
  311.                 case CALDAV_MODIFY_ITEM:
  312.                     if (mismatch) {
  313.                         LOG("CalDAV: etag mismatch in modifyItem");
  314.                         thisCalendar.promptOverwrite(aMethod, aItem,
  315.                                                      aListener, aOldItem);
  316.                     } else {
  317.                         thisCalendar.performModifyItem(aItem, aOldItem, aListener);
  318.                     }
  319.                     break;
  320.  
  321.                 case CALDAV_DELETE_ITEM:
  322.                     if (mismatch) {
  323.                         LOG("CalDAV: etag mismatch in deleteItem");
  324.                         thisCalendar.promptOverwrite(aMethod, aItem,
  325.                                                      aListener, null);
  326.                     } else {
  327.                         thisCalendar.performDeleteItem(aItem, aListener);
  328.                     }
  329.                     break;
  330.  
  331.                 default:
  332.                     thisCalendar.mEtagCache[aItem.id] = serverEtag;
  333.                     break;
  334.             }
  335.         }
  336.  
  337.         listener.onOperationDetail = function OOD(aStatusCode, aResource,
  338.                                                   aOperation, aDetail,
  339.                                                   aClosure) {
  340.             LOG("fetchEtag: onOperationDetail aStatusCode=" + aStatusCode);
  341.  
  342.             var props = aDetail.QueryInterface(Components.interfaces.nsIProperties);
  343.             serverEtag = props.get("DAV: getetag",
  344.                                    Components.interfaces.nsISupportsString).toString();
  345.         }
  346.  
  347.         var webdavSvc = Components.classes['@mozilla.org/webdav/service;1'].
  348.                         getService(Components.interfaces.nsIWebDAVService);
  349.         webdavSvc.getResourceProperties(itemResource, 1, ["DAV: getetag"],
  350.                                         false, listener, this, null);
  351.     },
  352.  
  353.     promptOverwrite: function caldavPO(aMethod, aItem, aListener, aOldItem) {
  354.         var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].
  355.                             getService(Components.interfaces.nsIPromptService);
  356.  
  357.         var promptTitle = calGetString("calendar", "itemModifiedOnServerTitle");
  358.         var promptMessage = calGetString("calendar", "itemModifiedOnServer");
  359.         var buttonLabel1;
  360.  
  361.         if (aMethod == CALDAV_MODIFY_ITEM) {
  362.             promptMessage += calGetString("calendar", "modifyWillLoseData");
  363.             buttonLabel1 = calGetString("calendar", "proceedModify");
  364.         } else {
  365.             promptMessage += calGetString("calendar", "deleteWillLoseData");
  366.             buttonLabel1 = calGetString("calendar", "proceedDelete");
  367.         }
  368.         
  369.         var buttonLabel2 = calGetString("calendar", "updateFromServer");
  370.  
  371.         var flags = promptService.BUTTON_TITLE_IS_STRING *
  372.                     promptService.BUTTON_POS_0 +
  373.                     promptService.BUTTON_TITLE_IS_STRING *
  374.                     promptService.BUTTON_POS_1;
  375.  
  376.         var choice = promptService.confirmEx(null, promptTitle, promptMessage,
  377.                                              flags, buttonLabel1, buttonLabel2,
  378.                                              null, null, {});
  379.  
  380.         if (choice == 0) {
  381.             if (aMethod == CALDAV_MODIFY_ITEM) {
  382.                 this.performModifyItem(aItem, aOldItem, aListener);
  383.             } else {
  384.                 this.performDeleteItem(aItem, aListener);
  385.             }
  386.         } else {
  387.             this.getUpdatedItem(aItem, aListener);
  388.         }
  389.  
  390.     },
  391.  
  392.     mEtagCache: null,
  393.  
  394.     /**
  395.      * addItem() is required by the IDL, but simply calls adoptItem().
  396.      * Actually adding the item to the CalDAV store takes place in
  397.      * performAdoptItem().
  398.      *
  399.      * @param aItem       item to check
  400.      * @param aListener   listener for method completion
  401.      */
  402.     addItem: function caldavAI(aItem, aListener) {
  403.         var newItem = aItem.clone();
  404.         return this.adoptItem(newItem, aListener);
  405.     },
  406.     
  407.     /**
  408.      * Sends data regarding the requested new item off for etag checking.
  409.      *
  410.      * @param aItem       item to check
  411.      * @param aListener   listener for method completion
  412.      */
  413.     adoptItem: function caldavAI2(aItem, aListener) {
  414.         this.fetchEtag(CALDAV_ADOPT_ITEM, aItem, aListener, null);
  415.     },
  416.  
  417.     /**
  418.      * Performs the actual addition of the item to CalDAV store, after etag
  419.      * checking.
  420.      *
  421.      * @param aItem       item to check
  422.      * @param aListener   listener for method completion
  423.      */
  424.     performAdoptItem: function caldavPAI(aItem, aListener) {
  425.         if (aItem.id == null && aItem.isMutable) {
  426.             aItem.id = getUUID();
  427.         }
  428.  
  429.         if (aItem.id == null) {
  430.             if (aListener)
  431.                 aListener.onOperationComplete (this,
  432.                                                Components.results.NS_ERROR_FAILURE,
  433.                                                aListener.ADD,
  434.                                                aItem.id,
  435.                                                "Can't set ID on non-mutable item to addItem");
  436.             return;
  437.         }
  438.  
  439.         // XXX how are we REALLY supposed to figure this out?
  440.         var locationPath = aItem.id + ".ics";
  441.         var itemUri = this.mCalendarUri.clone();
  442.         itemUri.spec = this.makeUri(locationPath);
  443.         LOG("itemUri.spec = " + itemUri.spec);
  444.         var eventResource = new WebDavResource(itemUri);
  445.  
  446.         var listener = new WebDavListener();
  447.         var thisCalendar = this;
  448.         listener.onOperationComplete = 
  449.         function onPutComplete(aStatusCode, aResource, aOperation, aClosure) {
  450.  
  451.             // 201 = HTTP "Created"
  452.             //
  453.             if (aStatusCode == 201) {
  454.                 LOG("Item added successfully");
  455.  
  456.                 var retVal = Components.results.NS_OK;
  457.                 // CalDAV does not require that the etag returned on PUT will
  458.                 // be identical to the etag returned on later operations
  459.                 // (CalDAV 5.3.4), so we best re-query.
  460.                 thisCalendar.fetchEtag(CALDAV_CACHE_ETAG, aItem, null, null);
  461.  
  462.             } else if (aStatusCode == 200) {
  463.                 LOG("CalDAV: 200 received from server: server malfunction");
  464.                 retVal = Components.results.NS_ERROR_FAILURE;
  465.             } else {
  466.                 if (aStatusCode > 999) {
  467.                     aStatusCode = "0x" + aStatusCode.toString(16);
  468.                 }
  469.  
  470.                 // XXX real error handling
  471.                 LOG("Error adding item: " + aStatusCode);
  472.                 retVal = Components.results.NS_ERROR_FAILURE;
  473.             }
  474.  
  475.             // notify the listener
  476.             if (aListener) {
  477.                 try {
  478.                     aListener.onOperationComplete(thisCalendar,
  479.                                                   retVal,
  480.                                                   aListener.ADD,
  481.                                                   aItem.id,
  482.                                                   aItem);
  483.                 } catch (ex) {
  484.                     LOG("addItem's onOperationComplete threw an exception "
  485.                           + ex + "; ignoring");
  486.                 }
  487.             }
  488.  
  489.             // notify observers
  490.             if (Components.isSuccessCode(retVal)) {
  491.                 thisCalendar.mObservers.notify("onAddItem", [aItem]);
  492.             }
  493.         }
  494.   
  495.         aItem.calendar = this;
  496.         aItem.generation = 1;
  497.         aItem.setProperty("X-MOZ-LOCATIONPATH", locationPath);
  498.         aItem.makeImmutable();
  499.  
  500.         LOG("icalString = " + aItem.icalString);
  501.  
  502.         // XXX use if not exists
  503.         // do WebDAV put
  504.         var webSvc = Components.classes['@mozilla.org/webdav/service;1']
  505.             .getService(Components.interfaces.nsIWebDAVService);
  506.         webSvc.putFromString(eventResource, "text/calendar; charset=utf-8",
  507.                              aItem.icalString, listener, this, null);
  508.  
  509.         return;
  510.     },
  511.  
  512.     /**
  513.      * Sends info about the request to modify an item off for etag checking.
  514.      *
  515.      * @param aItem       item to check
  516.      * @param aOldItem    aOldItem argument in modifyItem requests
  517.      * @param aListener   listener from original request
  518.      */
  519.     modifyItem: function caldavMI(aItem, aOldItem, aListener) {
  520.         this.fetchEtag(CALDAV_MODIFY_ITEM, aItem, aListener, aOldItem);
  521.     },
  522.  
  523.     /**
  524.      * Modifies existing item in CalDAV store.
  525.      *
  526.      * @param aItem       item to check
  527.      * @param aOldItem    previous version of item to be modified
  528.      * @param aListener   listener from original request
  529.      */
  530.     performModifyItem: function caldavPMI(aNewItem, aOldItem, aListener) {
  531.  
  532.         if (aNewItem.id == null) {
  533.  
  534.             // XXXYYY fix to match iface spec
  535.             // this is definitely an error
  536.             if (aListener) {
  537.                 try {
  538.                     aListener.onOperationComplete(this,
  539.                                                   Components.results.NS_ERROR_FAILURE,
  540.                                                   aListener.MODIFY,
  541.                                                   aItem.id,
  542.                                                   "ID for modifyItem doesn't exist or is null");
  543.                 } catch (ex) {
  544.                     LOG("modifyItem's onOperationComplete threw an"
  545.                           + " exception " + ex + "; ignoring");
  546.                 }
  547.             }
  548.  
  549.             return;
  550.         }
  551.  
  552.         if (aNewItem.parentItem != aNewItem) {
  553.             aNewItem.parentItem.recurrenceInfo.modifyException(aNewItem);
  554.             aNewItem = aNewItem.parentItem;
  555.         }
  556.  
  557.         var eventUri = this.mCalendarUri.clone();
  558.         try {
  559.             eventUri.spec = this.makeUri(aNewItem.getProperty("X-MOZ-LOCATIONPATH"));
  560.             LOG("using X-MOZ-LOCATIONPATH: " + eventUri.spec);
  561.         } catch (ex) {
  562.             // XXX how are we REALLY supposed to figure this out?
  563.             eventUri.spec = this.makeUri(aNewItem.id + ".ics");
  564.         }
  565.  
  566.         // It seems redundant to use generation when we have etags
  567.         // but until the idl is changed we do it.
  568.         if (aOldItem.parentItem.generation != aNewItem.generation) {
  569.             if (aListener) {
  570.                 aListener.onOperationComplete (this,
  571.                                                Components.results.NS_ERROR_FAILURE,
  572.                                                aListener.MODIFY,
  573.                                                aNewItem.id,
  574.                                                "generation mismatch in modifyItem");
  575.             }
  576.             return;
  577.         }
  578.  
  579.         aNewItem.generation += 1;
  580.  
  581.         var eventResource = new WebDavResource(eventUri);
  582.  
  583.         var listener = new WebDavListener();
  584.         var thisCalendar = this;
  585.  
  586.         const icssvc = Components.classes["@mozilla.org/calendar/ics-service;1"].
  587.                        getService(Components.interfaces.calIICSService);
  588.         var modifiedItem = icssvc.createIcalComponent("VCALENDAR");
  589.         modifiedItem.prodid = "-//Mozilla Calendar//NONSGML Sunbird//EN";
  590.         modifiedItem.version = "2.0";
  591.         modifiedItem.addSubcomponent(aNewItem.icalComponent);
  592.         if (aNewItem.recurrenceInfo) {
  593.             var exceptions = aNewItem.recurrenceInfo.getExceptionIds({});
  594.             for each (var exc in exceptions) {
  595.                 modifiedItem.addSubcomponent(aNewItem.recurrenceInfo.getExceptionFor(exc, true).icalComponent);
  596.             }
  597.         }
  598.         var modifiedItemICS = modifiedItem.serializeToICS();
  599.  
  600.         listener.onOperationComplete = function(aStatusCode, aResource,
  601.                                                 aOperation, aClosure) {
  602.             // 201 = HTTP "Created"
  603.             // 204 = HTTP "No Content"
  604.             //
  605.             if (aStatusCode == 204 || aStatusCode == 201) {
  606.                 LOG("Item modified successfully.");
  607.                 var retVal = Components.results.NS_OK;
  608.                 // CalDAV does not require that the etag returned on PUT will
  609.                 // be identical to the etag returned on later operations
  610.                 // (CalDAV 5.3.4), so we best re-query.
  611.                 thisCalendar.fetchEtag(CALDAV_CACHE_ETAG, aNewItem, null, null);
  612.  
  613.             } else {
  614.                 if (aStatusCode > 999) {
  615.                     aStatusCode = "0x " + aStatusCode.toString(16);
  616.                 }
  617.                 LOG("Error modifying item: " + aStatusCode);
  618.  
  619.                 // XXX deal with non-existent item here, other
  620.                 // real error handling
  621.  
  622.                 // XXX aStatusCode will be 201 Created for a PUT on an item
  623.                 // that didn't exist before.
  624.  
  625.                 retVal = Components.results.NS_ERROR_FAILURE;
  626.             }
  627.  
  628.             // XXX ensure immutable version returned
  629.             // notify listener
  630.             if (aListener) {
  631.                 try {
  632.                     aListener.onOperationComplete(thisCalendar, retVal,
  633.                                                   aListener.MODIFY,
  634.                                                   aNewItem.id, aNewItem);
  635.                 } catch (ex) {
  636.                     LOG("modifyItem's onOperationComplete threw an"
  637.                           + " exception " + ex + "; ignoring");
  638.                 }
  639.             }
  640.  
  641.             // notify observers
  642.             if (Components.isSuccessCode(retVal)) {
  643.                 thisCalendar.mObservers.notify("onModifyItem",
  644.                                                [aNewItem, aOldItem.parentItem]);
  645.             }
  646.  
  647.             return;
  648.         }
  649.  
  650.         // XXX use if-exists stuff here
  651.         // XXX use etag as generation
  652.         // do WebDAV put
  653.         LOG("modifyItem: PUTting = " + modifiedItemICS);
  654.         var webSvc = Components.classes['@mozilla.org/webdav/service;1']
  655.             .getService(Components.interfaces.nsIWebDAVService);
  656.         webSvc.putFromString(eventResource, "text/calendar; charset=utf-8",
  657.                              modifiedItemICS, listener, this, null);
  658.  
  659.         return;
  660.     },
  661.  
  662.     /**
  663.      * Sends data regarding requested deletion off for etag checking.
  664.      *
  665.      * @param aItem       item to check
  666.      * @param aListener   listener for method completion
  667.      */
  668.     deleteItem: function caldavDI(aItem, aListener) {
  669.         this.fetchEtag(CALDAV_DELETE_ITEM, aItem, aListener, null);
  670.     },
  671.  
  672.     /**
  673.      * Deletes item from CalDAV store.
  674.      *
  675.      * @param aItem       item to delete
  676.      * @param aListener   listener for method completion
  677.      */
  678.     performDeleteItem: function caldavPDI(aItem, aListener) {
  679.  
  680.         if (aItem.id == null) {
  681.             if (aListener)
  682.                 aListener.onOperationComplete (this,
  683.                                                Components.results.NS_ERROR_FAILURE,
  684.                                                aListener.DELETE,
  685.                                                aItem.id,
  686.                                                "ID doesn't exist for deleteItem");
  687.             return;
  688.         }
  689.  
  690.         var eventUri = this.mCalendarUri.clone();
  691.         try {
  692.             eventUri.spec = this.makeUri(aItem.getProperty("X-MOZ-LOCATIONPATH"));
  693.             LOG("using X-MOZ-LOCATIONPATH: " + eventUri.spec);
  694.         } catch (ex) {
  695.             // XXX how are we REALLY supposed to figure this out?
  696.             eventUri.spec = this.makeUri(aItem.id + ".ics");
  697.         }
  698.  
  699.         var eventResource = new WebDavResource(eventUri);
  700.  
  701.         var listener = new WebDavListener();
  702.         var thisCalendar = this;
  703.  
  704.         listener.onOperationComplete = 
  705.         function onOperationComplete(aStatusCode, aResource, aOperation,
  706.                                      aClosure) {
  707.  
  708.             // 204 = HTTP "No content"
  709.             //
  710.             if (aStatusCode == 204) {
  711.                 delete thisCalendar.mEtagCache[aItem.id];
  712.                 LOG("Item deleted successfully.");
  713.                 var retVal = Components.results.NS_OK;
  714.             } else {
  715.                 LOG("Error deleting item: " + aStatusCode);
  716.                 // XXX real error handling here
  717.                 retVal = Components.results.NS_ERROR_FAILURE;
  718.             }
  719.  
  720.             // notify the listener
  721.             if (aListener) {
  722.                 try {
  723.                     aListener.onOperationComplete(thisCalendar,
  724.                                                   Components.results.NS_OK,
  725.                                                   aListener.DELETE,
  726.                                                   aItem.id,
  727.                                                   null);
  728.                 } catch (ex) {
  729.                     LOG("deleteItem's onOperationComplete threw an"
  730.                           + " exception " + ex + "; ignoring");
  731.                 }
  732.             }
  733.  
  734.             // notify observers
  735.             if (Components.isSuccessCode(retVal)) {
  736.                 thisCalendar.mObservers.notify("onDeleteItem", [aItem]);
  737.             }
  738.         }
  739.  
  740.         // XXX check generation
  741.         // do WebDAV remove
  742.         var webSvc = Components.classes['@mozilla.org/webdav/service;1']
  743.             .getService(Components.interfaces.nsIWebDAVService);
  744.         webSvc.remove(eventResource, listener, this, null);
  745.  
  746.         return;
  747.     },
  748.  
  749.     /**
  750.      * Retrieves a specific item from the CalDAV store.
  751.      * Use when an outdated copy of the item is in hand.
  752.      *
  753.      * @param aItem       item to fetch
  754.      * @param aListener   listener for method completion
  755.      */
  756.     getUpdatedItem: function caldavGUI(aItem, aListener) {
  757.         if (!aListener) {
  758.             return;
  759.         }
  760.  
  761.         if (aItem == null) {
  762.             aListener.onOperationComplete(this,
  763.                                           Components.results.NS_ERROR_FAILURE,
  764.                                           aListener.GET,
  765.                                           null,
  766.                                           "passed in null item");
  767.             return;
  768.         }
  769.  
  770.         var itemType = "VEVENT";
  771.         if (aItem instanceof Components.interfaces.calITodo) {
  772.             itemType = "VTODO";
  773.         }
  774.  
  775.         var C = new Namespace("C", "urn:ietf:params:xml:ns:caldav");
  776.         var D = new Namespace("D", "DAV:");
  777.         default xml namespace = C;
  778.  
  779.         queryXml =
  780.           <calendar-query xmlns:D="DAV:">
  781.             <D:prop>
  782.               <D:getetag/>
  783.               <calendar-data/>
  784.             </D:prop>
  785.             <filter>
  786.               <comp-filter name="VCALENDAR">
  787.                 <comp-filter name={itemType}>
  788.                   <prop-filter name="UID">
  789.                     <text-match collation="i;octet">
  790.                       {aItem.id}
  791.                     </text-match>
  792.                   </prop-filter>
  793.                 </comp-filter>
  794.               </comp-filter>
  795.             </filter>
  796.           </calendar-query>;
  797.  
  798.         this.reportInternal(xmlHeader + queryXml.toXMLString(),
  799.                             false, null, null, 1, aListener, aItem);
  800.         return;
  801.  
  802.     },
  803.  
  804.     // void getItem( in string id, in calIOperationListener aListener );
  805.     getItem: function (aId, aListener) {
  806.  
  807.         if (!aListener)
  808.             return;
  809.  
  810.         if (aId == null) {
  811.             aListener.onOperationComplete(this,
  812.                                           Components.results.NS_ERROR_FAILURE,
  813.                                           aListener.GET,
  814.                                           null,
  815.                                           "passed in empty iid");
  816.             return;
  817.         }
  818.  
  819.  
  820.         // this is our basic search-by-uid xml
  821.         // XXX get rid of vevent filter?
  822.         // XXX need a prefix in the namespace decl?
  823.         default xml namespace = "urn:ietf:params:xml:ns:caldav";
  824.         var D = new Namespace("D", "DAV:");
  825.         queryXml = 
  826.           <calendar-query xmlns:D="DAV:">
  827.             <D:prop>
  828.               <calendar-data/>
  829.             </D:prop>
  830.             <filter>
  831.               <comp-filter name="VCALENDAR">
  832.                 <comp-filter name="VEVENT">
  833.                   <prop-filter name="UID">
  834.                     <text-match caseless="no">
  835.                       {aId}
  836.                     </text-match>
  837.                   </prop-filter>
  838.                 </comp-filter>
  839.               </comp-filter>
  840.             </filter>
  841.           </calendar-query>;
  842.  
  843.         this.reportInternal(xmlHeader + queryXml.toXMLString(), 
  844.                             false, null, null, 1, aListener);
  845.         return;
  846.     },
  847.  
  848.     reportInternal: function caldavRI(aQuery, aOccurrences, aRangeStart,
  849.                                       aRangeEnd, aCount, aListener, aItem,
  850.                                       aQueryCount, aQueryStatuses)
  851.     {
  852.         var reportListener = new WebDavListener();
  853.         var count = 0;  // maximum number of hits to return
  854.         var thisCalendar = this; // need to access from inside the callback
  855.  
  856.         reportListener.onOperationDetail = function(aStatusCode, aResource,
  857.                                                     aOperation, aDetail,
  858.                                                     aClosure) {
  859.             var rv;
  860.             var errString;
  861.  
  862.             // is this detail the entire search result, rather than a single
  863.             // detail within a set?
  864.             //
  865.             if (aResource.path == calendarDirUri.path) {
  866.                 // XXX is this even valid?  what should we do here?
  867.                 // XXX if it's an error, it might be valid?
  868.                 LOG("XXX report result for calendar, not event\n");
  869.                 throw("XXX report result for calendar, not event\n");
  870.             }
  871.  
  872.             var items = null;
  873.  
  874.             // XXX need to do better than looking for just 200
  875.             if (aStatusCode == 200) {
  876.  
  877.                 // we've already called back the maximum number of hits, so 
  878.                 // we're done here.
  879.                 // 
  880.                 if (aCount && count >= aCount) {
  881.                     return;
  882.                 }
  883.                 ++count;
  884.  
  885.                 // aDetail is the response element from the multi-status
  886.                 // XXX try-catch
  887.                 var xSerializer = Components.classes
  888.                     ['@mozilla.org/xmlextras/xmlserializer;1']
  889.                     .getService(Components.interfaces.nsIDOMSerializer);
  890.                 // libical needs to see \r\n instead on \n\n in the case of "folded" lines
  891.                 var response = xSerializer.serializeToString(aDetail).replace(/\n\n/g, "\r\n");
  892.                 var responseElement = new XML(response);
  893.  
  894.                 // create calIItemBase from e4x object
  895.                 // XXX error-check that we only have one result, etc
  896.                 var C = new Namespace("urn:ietf:params:xml:ns:caldav");
  897.                 var D = new Namespace("DAV:");
  898.  
  899.                 var etag = responseElement..D::["getetag"];
  900.  
  901.                 // cause returned data to be parsed into the item
  902.                 var calData = responseElement..C::["calendar-data"];
  903.                 if (!calData.toString().length) {
  904.                   Components.utils.reportError(
  905.                     "Empty or non-existent <calendar-data> element returned" +
  906.                     " by CalDAV server for URI <" + aResource.spec +
  907.                     ">; ignoring");
  908.                   return;
  909.                 }
  910.                 LOG("item result = \n" + calData);
  911.                 if (!thisCalendar.mICSService) {
  912.                     thisCalendar.mICSService = Components.classes["@mozilla.org/calendar/ics-service;1"].
  913.                                                getService(Components.interfaces.calIICSService);
  914.                 }
  915.                 var rootComp = thisCalendar.mICSService.parseICS(calData);
  916.  
  917.                 var calComp;
  918.                 if (rootComp.componentType == 'VCALENDAR') {
  919.                     calComp = rootComp;
  920.                 } else {
  921.                     calComp = rootComp.getFirstSubcomponent('VCALENDAR');
  922.                 }
  923.  
  924.                 var unexpandedItems = [];
  925.                 var uid2parent = {};
  926.                 var excItems = [];
  927.  
  928.                 while (calComp) {
  929.                     // Get unknown properties
  930.                     var prop = calComp.getFirstProperty("ANY");
  931.                     while (prop) {
  932.                         thisCalendar.unmappedProperties.push(prop);
  933.                         prop = calComp.getNextProperty("ANY");
  934.                     }
  935.  
  936.                     var subComp = calComp.getFirstSubcomponent("ANY");
  937.                     while (subComp) {
  938.                         // Place each subcomp in a try block, to hopefully get as
  939.                         // much of a bad calendar as possible
  940.                         try {
  941.                             var item = null;
  942.                             switch (subComp.componentType) {
  943.                             case "VEVENT":
  944.                                 item = Components.classes["@mozilla.org/calendar/event;1"].
  945.                                        createInstance(Components.interfaces.calIEvent);
  946.                                 break;
  947.                             case "VTODO":
  948.                                 item = Components.classes["@mozilla.org/calendar/todo;1"].
  949.                                        createInstance(Components.interfaces.calITodo);
  950.                                 break;
  951.                             case "VTIMEZONE":
  952.                                 // we should already have this, so there's no need to
  953.                                 // do anything with it here.
  954.                                 break;
  955.                             default:
  956.                                 thisCalendar.unmappedComponents.push(subComp);
  957.                                 break;
  958.                             }
  959.                             if (item != null) {
  960.  
  961.                                 item.icalComponent = subComp;
  962.                                 // save the location name in case we need to modify
  963.                                 // need to build using thisCalendar since aResource.spec
  964.                                 // won't contain any auth info embedded in the URI
  965.                                 var locationPath = decodeURIComponent(aResource.path)
  966.                                                    .substr(thisCalendar.mLocationPath.length);
  967.                                 item.setProperty("X-MOZ-LOCATIONPATH", locationPath);
  968.  
  969.                                 var rid = item.recurrenceId;
  970.                                 if (rid == null) {
  971.                                     unexpandedItems.push( item );
  972.                                     if (item.recurrenceInfo != null) {
  973.                                         uid2parent[item.id] = item;
  974.                                     }
  975.                                 } else {
  976.                                     item.calendar = thisCalendar;
  977.                                     // force no recurrence info so we can
  978.                                     // rebuild it cleanly below
  979.                                     item.recurrenceInfo = null;
  980.                                     excItems.push(item);
  981.                                 }
  982.                             }
  983.                         } catch (ex) { 
  984.                             thisCalendar.mObservers.notify("onError", [ex.result, ex.toString()]);
  985.                         }
  986.                         subComp = calComp.getNextSubcomponent("ANY");
  987.                     }
  988.                     calComp = rootComp.getNextSubcomponent('VCALENDAR');
  989.                 }
  990.  
  991.                 // tag "exceptions", i.e. items with rid:
  992.                 for each (var item in excItems) {
  993.                     var parent = uid2parent[item.id];
  994.                     if (parent == null) {
  995.                         LOG( "no parent item for rid=" + item.recurrenceId );
  996.                     } else {
  997.                         item.parentItem = parent;
  998.                         item.parentItem.recurrenceInfo.modifyException(item);
  999.                     }
  1000.                 }
  1001.                 // if we loop over both excItems and unexpandedItems using 'item'
  1002.                 // we can be confident that 'item' means something below
  1003.                 for each (var item in unexpandedItems) {
  1004.                     item.calendar = thisCalendar;
  1005.                 }
  1006.  
  1007.                 thisCalendar.mEtagCache[item.id] = etag;
  1008.                 if (aItem) {
  1009.                     // if aItem is not null, we were called from
  1010.                     // getUpdatedItem(), and the view isn't listening to any
  1011.                     // changes. So in order to have the updated item displayed
  1012.                     // we need to modify the item currently displayed with
  1013.                     // the one just fetched
  1014.                     thisCalendar.mObservers.notify("onModifyItem",
  1015.                                                    [item, aItem.parentItem]);
  1016.                 }
  1017.  
  1018.                 // figure out what type of item to return
  1019.                 var iid;
  1020.                 if(aOccurrences) {
  1021.                     iid = Components.interfaces.calIItemBase;
  1022.                     if (item.recurrenceInfo) {
  1023.                         LOG("ITEM has recurrence: " + item + " (" + item.title + ")");
  1024.                         LOG("rangestart: " + aRangeStart.jsDate + " -> " + aRangeEnd.jsDate);
  1025.                         // XXX does getOcc call makeImmutable?
  1026.                         items = item.recurrenceInfo.getOccurrences(aRangeStart,
  1027.                                                                    aRangeEnd,
  1028.                                                                    0, {});
  1029.                     } else {
  1030.                         // XXX need to make occurrences immutable?
  1031.                         items = [ item ];
  1032.                     }
  1033.                     rv = Components.results.NS_OK;
  1034.                 } else if (item instanceof Components.interfaces.calIEvent) {
  1035.                     iid = Components.interfaces.calIEvent;
  1036.                     rv = Components.results.NS_OK;
  1037.                     items = [ item ];
  1038.                 } else if (item instanceof Components.interfaces.calITodo) {
  1039.                     iid = Components.interfaces.calITodo;
  1040.                     rv = Components.results.NS_OK;
  1041.                     items = [ item ];
  1042.                 } else {
  1043.                     errString = "Can't deduce item type based on query";
  1044.                     rv = Components.results.NS_ERROR_FAILURE;
  1045.                 }
  1046.  
  1047.             } else { 
  1048.                 // XXX
  1049.                 LOG("aStatusCode = " + aStatusCode);
  1050.                 errString = "XXX";
  1051.                 rv = Components.results.NS_ERROR_FAILURE;
  1052.             }
  1053.  
  1054.             // XXX  handle aCount
  1055.             if (errString) {
  1056.                 LOG("errString = " + errString);
  1057.             }
  1058.  
  1059.             try {
  1060.                 aListener.onGetResult(thisCalendar, rv, iid, null,
  1061.                                       items ? items.length : 0,
  1062.                                       errString ? errString : items);
  1063.             } catch (ex) {
  1064.                     LOG("reportInternal's onGetResult threw an"
  1065.                           + " exception " + ex + "; ignoring");
  1066.             }
  1067.  
  1068.             // We have a result, so we must be authenticated
  1069.             if (thisCalendar.mAuthenticationStatus == kCaldavFirstRequestSent) {
  1070.                 thisCalendar.mAuthenticationStatus = kCaldavFreshlyAuthenticated;
  1071.             }
  1072.  
  1073.             if (thisCalendar.mAuthenticationStatus == kCaldavFreshlyAuthenticated) {
  1074.                 thisCalendar.mAuthenticationStatus = kCaldavAuthenticated;
  1075.                 while (thisCalendar.mPendingStartupRequests.length > 0) {
  1076.                     thisCalendar.popStartupRequest();
  1077.                 }
  1078.             }
  1079.             return;
  1080.         };
  1081.  
  1082.         reportListener.onOperationComplete = function(aStatusCode, aResource,
  1083.                                                       aOperation, aClosure) {
  1084.             aQueryStatuses.push(aStatusCode);
  1085.             if (aQueryStatuses.length == aQueryCount) {
  1086.                 var rv = Components.results.NS_OK;
  1087.                 var errString;
  1088.  
  1089.                 for each (statusCode in aQueryStatuses) {
  1090.                     if (statusCode != 200) { // XXX better error checking
  1091.                         rv = Components.results.NS_ERROR_FAILURE;
  1092.                         errString = "XXX something bad happened";
  1093.                     }
  1094.                 }
  1095.  
  1096.                 // call back the listener
  1097.                 try {
  1098.                     if (aListener) {
  1099.                     aListener.onOperationComplete(thisCalendar,
  1100.                                                   Components.results.
  1101.                                                   rv, aListener.GET, null,
  1102.                                                   errString);
  1103.                     }
  1104.                 } catch (ex) {
  1105.                     LOG("reportInternal's onOperationComplete threw an"
  1106.                         + " exception " + ex + "; ignoring");
  1107.                 }
  1108.  
  1109.                 return;
  1110.             } else {
  1111.                 // there's still a query pending, so it's too early to 
  1112.                 // call the listener back
  1113.             }
  1114.         };
  1115.  
  1116.         // convert this into a form the WebDAV service can use
  1117.         var xParser = Components.classes['@mozilla.org/xmlextras/domparser;1']
  1118.                       .getService(Components.interfaces.nsIDOMParser);
  1119.         queryDoc = xParser.parseFromString(aQuery, "application/xml");
  1120.  
  1121.         // construct the resource we want to search against
  1122.         var calendarDirUri = this.mCalendarUri.clone();
  1123.         calendarDirUri.spec = this.makeUri('');
  1124.         LOG("report uri = " + calendarDirUri.spec);
  1125.         var calendarDirResource = new WebDavResource(calendarDirUri);
  1126.  
  1127.         var webSvc = Components.classes['@mozilla.org/webdav/service;1']
  1128.             .getService(Components.interfaces.nsIWebDAVService);
  1129.         webSvc.report(calendarDirResource, queryDoc, true, reportListener,
  1130.                       this, null);
  1131.         return;    
  1132.  
  1133.     },
  1134.  
  1135.  
  1136.     // void getItems( in unsigned long aItemFilter, in unsigned long aCount, 
  1137.     //                in calIDateTime aRangeStart, in calIDateTime aRangeEnd,
  1138.     //                in calIOperationListener aListener );
  1139.     getItems: function (aItemFilter, aCount, aRangeStart, aRangeEnd, aListener)
  1140.     {
  1141.         if (!aListener) {
  1142.             return;
  1143.         }
  1144.  
  1145.         if (this.mDisabled) {
  1146.             var errString = "calendar " + this.name + " is disabled";
  1147.             aListener.onOperationComplete(this,
  1148.                                           Components.results.NS_ERROR_FAILURE,
  1149.                                           aListener.GET, null, errString);
  1150.             while (this.mPendingStartupRequests.length > 0) {
  1151.                 this.popStartupRequest();
  1152.             }
  1153.             // check if maybe our calendar has become available
  1154.             this.checkDavResourceType();
  1155.             return;
  1156.         }
  1157.  
  1158.         if (this.mAuthenticationStatus == kCaldavNoAuthentication) {
  1159.            this.mAuthenticationStatus = kCaldavFirstRequestSent;
  1160.            this.checkDavResourceType();
  1161.         }
  1162.  
  1163.         if (this.mAuthenticationStatus == kCaldavFirstRequestSent) {
  1164.             var req = new Array(aItemFilter, aCount, aRangeStart, aRangeEnd, aListener);
  1165.             this.mPendingStartupRequests.push(req);
  1166.             return;
  1167.         }
  1168.  
  1169.         // this is our basic report xml
  1170.         var C = new Namespace("C", "urn:ietf:params:xml:ns:caldav");
  1171.         var D = new Namespace("D", "DAV:");
  1172.         default xml namespace = C;
  1173.  
  1174.         var queryXml = 
  1175.           <calendar-query xmlns:D={D}>
  1176.             <D:prop>
  1177.               <D:getetag/>
  1178.               <calendar-data/>
  1179.             </D:prop>
  1180.             <filter>
  1181.               <comp-filter name="VCALENDAR">
  1182.                 <comp-filter/>
  1183.               </comp-filter>
  1184.             </filter>
  1185.           </calendar-query>;
  1186.  
  1187.         var compFilterNames = new Array();
  1188.         compFilterNames[calICalendar.ITEM_FILTER_TYPE_TODO] = "VTODO";
  1189.         // omit VJOURNAL for now since we don't support it and not all 
  1190.         // CalDAV servers can handle requests for it
  1191.         // compFilterNames[calICalendar.ITEM_FILTER_TYPE_JOURNAL] = "VJOURNAL";
  1192.         compFilterNames[calICalendar.ITEM_FILTER_TYPE_EVENT] = "VEVENT";
  1193.  
  1194.         var queryFilters = [];
  1195.         for (var i in compFilterNames) {
  1196.             if (aItemFilter & i) {
  1197.                 queryFilters.push(compFilterNames[i]);
  1198.             }
  1199.         }
  1200.  
  1201.         if (queryFilters.length < 1) {
  1202.             LOG("No item types specified");
  1203.             // XXX should we just quietly call back the completion method?
  1204.             throw NS_ERROR_FAILURE;
  1205.         }
  1206.  
  1207.         var queryCount = queryFilters.length;
  1208.         var queryStatuses = new Array();
  1209.  
  1210.         // if a time range has been specified, do the appropriate restriction.
  1211.         // XXX express "end of time" in caldav by leaving off "start", "end"
  1212.         var hasRange = false;
  1213.         if (aRangeStart && aRangeStart.isValid && 
  1214.             aRangeEnd && aRangeEnd.isValid) {
  1215.  
  1216.             var hasRange = true;
  1217.             var queryRangeStart = aRangeStart.clone();
  1218.             var queryRangeEnd = aRangeEnd.clone();
  1219.             queryRangeStart.isDate = false;
  1220.             if (queryRangeEnd.isDate) {
  1221.                 // add a day to rangeEnd since we want to match events all that day
  1222.                 // and isDate=false is converting the date to midnight
  1223.                 queryRangeEnd.day++;
  1224.                 queryRangeEnd.isDate = false;
  1225.             }
  1226.             var rangeXml = <time-range start={queryRangeStart.getInTimezone("UTC").icalString}
  1227.                                        end={queryRangeEnd.getInTimezone("UTC").icalString}/>;
  1228.  
  1229.         }
  1230.  
  1231.         for (var queryFilter in queryFilters) {
  1232.  
  1233.             var typeQueryXml = queryXml;
  1234.  
  1235.             typeQueryXml[0].C::filter.C::["comp-filter"]
  1236.                            .C::["comp-filter"] =
  1237.                            <comp-filter name={queryFilters[queryFilter]}/>;
  1238.             //see http://tools.ietf.org/html/rfc4791#section-7.8.9
  1239.             if(queryFilters[queryFilter] == "VTODO" ){
  1240.                 if((aItemFilter &  calICalendar.ITEM_FILTER_COMPLETED_YES) != 1 ) {
  1241.                     var filter_todo = 
  1242.                         <prop-filter name="COMPLETED">
  1243.                             <is-not-defined/>
  1244.                         </prop-filter>;
  1245.                     // append the prop-filter as a child of our innermost comp-filter
  1246.                     typeQueryXml[0].C::filter.C::["comp-filter"]
  1247.                         .C::["comp-filter"].appendChild(filter_todo);
  1248.                 }
  1249.              } 
  1250.  
  1251.             // append the time-range as a child of our innermost comp-filter
  1252.             if (hasRange) {
  1253.                 typeQueryXml[0].C::filter.C::["comp-filter"]
  1254.                                .C::["comp-filter"].appendChild(rangeXml);
  1255.             }
  1256.  
  1257.             var queryString = xmlHeader + typeQueryXml.toXMLString();
  1258.             LOG("getItems(): querying CalDAV server:\n" + queryString);
  1259.  
  1260.             var occurrences = (aItemFilter &
  1261.                               calICalendar.ITEM_FILTER_CLASS_OCCURRENCES) != 0;
  1262.             this.reportInternal(queryString, occurrences, aRangeStart,
  1263.                                 aRangeEnd, aCount, aListener, null, queryCount,
  1264.                                 queryStatuses);
  1265.         }
  1266.  
  1267.     },
  1268.  
  1269.     startBatch: function ()
  1270.     {
  1271.         this.mObservers.notify("onStartBatch");
  1272.     },
  1273.     endBatch: function ()
  1274.     {
  1275.         this.mObservers.notify("onEndBatch");
  1276.     },
  1277.  
  1278.     // nsIInterfaceRequestor impl
  1279.     getInterface: function(iid) {
  1280.         if (iid.equals(Components.interfaces.nsIAuthPrompt)) {
  1281.             return new calAuthPrompt();
  1282.         }
  1283.         else if (iid.equals(Components.interfaces.nsIPrompt)) {
  1284.             // use the window watcher service to get a nsIPrompt impl
  1285.             return Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
  1286.                              .getService(Components.interfaces.nsIWindowWatcher)
  1287.                              .getNewPrompter(null);
  1288.         } else if (iid.equals(Components.interfaces.nsIProgressEventSink)) {
  1289.             return this;
  1290.         // Needed for Lightning on branch vvv
  1291.         } else if (iid.equals(Components.interfaces.nsIDocShellTreeItem)) {
  1292.             return this;
  1293.         } else if (iid.equals(Components.interfaces.nsIAuthPromptProvider)) {
  1294.             return Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
  1295.                              .getService(Components.interfaces.nsIWindowWatcher)
  1296.                              .getNewPrompter(null);
  1297.         } else if (!isOnBranch && iid.equals(Components.interfaces.nsIAuthPrompt2)) {
  1298.             return new calAuthPrompt();
  1299.         }
  1300.         throw Components.results.NS_ERROR_NO_INTERFACE;
  1301.     },
  1302.  
  1303.     //
  1304.     // Helper functions
  1305.     //
  1306.  
  1307.     // Unless an error number is in this array, we consider it very bad, set
  1308.     // the calendar to readOnly, and give up.
  1309.     acceptableErrorNums: [],
  1310.  
  1311.     onError: function caldav_onError(aErrNo, aMessage) {
  1312.         var errorIsOk = false;
  1313.         for each (num in this.acceptableErrorNums) {
  1314.             if (num == aErrNo) {
  1315.                 errorIsOk = true;
  1316.                 break;
  1317.             }
  1318.         }
  1319.         if (!errorIsOk) {
  1320.             this.mReadOnly = true;
  1321.             this.mDisabled = true;
  1322.         }
  1323.  
  1324.         var paramBlock = Components.classes["@mozilla.org/embedcomp/dialogparam;1"]
  1325.                                    .createInstance(Components.interfaces
  1326.                                    .nsIDialogParamBlock);
  1327.         paramBlock.SetNumberStrings(3);
  1328.  
  1329.         var promptMessage = calGetString("calendar", "disabledMode", [this.name]);
  1330.         paramBlock.SetString(0, promptMessage);
  1331.         var errCode = "0x"+aErrNo.toString(16);
  1332.         paramBlock.SetString(1, errCode);
  1333.         paramBlock.SetString(2, aMessage);
  1334.         var wWatcher = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
  1335.                                  .getService(Components.interfaces.nsIWindowWatcher);
  1336.         wWatcher.openWindow(null,
  1337.                             "chrome://calendar/content/calErrorPrompt.xul",
  1338.                             "_blank",
  1339.                             "chrome,dialog=yes",
  1340.                             paramBlock);
  1341.  
  1342.         if (this.mDisabled && this.mPendingStartupRequests.length > 0) {
  1343.             this.popStartupRequest();
  1344.         }
  1345.     },
  1346.  
  1347.     popStartupRequest: function popStartupRequest() {
  1348.         var req = this.mPendingStartupRequests.pop();
  1349.         this.getItems(req[0], req[1], req[2], req[3], req[4]);
  1350.     },
  1351.  
  1352.     /**
  1353.      * Checks that the calendar URI exists and is a CalDAV calendar
  1354.      *
  1355.      */
  1356.     checkDavResourceType: function checkDavResourceType() {
  1357.         var listener = new WebDavListener();
  1358.         var resourceTypeXml = null;
  1359.         var resourceType = kDavResourceTypeNone;
  1360.         var thisCalendar = this;
  1361.         listener.onOperationComplete =
  1362.             function checkDavResourceType_oOC(aStatusCode, aResource,
  1363.                                               aOperation, aClosure) {
  1364.  
  1365.             if ((resourceType == null || resourceType == kDavResourceTypeNone) &&
  1366.                 !thisCalendar.mDisabled) {
  1367.                 thisCalendar.reportDavError(Components.interfaces.calIErrors.DAV_NOT_DAV,
  1368.                                             "dav_notDav");
  1369.             }
  1370.  
  1371.             if ((resourceType == kDavResourceTypeCollection) &&
  1372.                 !thisCalendar.mDisabled) {
  1373.                 thisCalendar.reportDavError(Components.interfaces.calIErrors.DAV_DAV_NOT_CALDAV,
  1374.                                             "dav_davNotCaldav");
  1375.             }
  1376.  
  1377.             // if this calendar was previously offline we want to recover
  1378.             if ((resourceType == kDavResourceTypeCalendar) &&
  1379.                 thisCalendar.mDisabled) {
  1380.                 thisCalendar.mDisabled = false;
  1381.                 thisCalendar.mReadOnly = false;
  1382.             }
  1383.  
  1384.             // we've authenticated in the process of PROPFINDing and can flush
  1385.             // the getItems request queue
  1386.             thisCalendar.mAuthenticationStatus = kCaldavFreshlyAuthenticated;
  1387.             if (thisCalendar.mPendingStartupRequests.length > 0) {
  1388.                 thisCalendar.popStartupRequest();
  1389.             }
  1390.         }
  1391.  
  1392.         listener.onOperationDetail =
  1393.             function checkDavResourceType_oOD(aStatusCode, aResource,
  1394.                                               aOperation, aDetail, aClosure) {
  1395.  
  1396.             var prop = aDetail.QueryInterface(Components.interfaces.nsIProperties);
  1397.  
  1398.             try {
  1399.                 resourceTypeXml = prop.get("DAV: resourcetype",
  1400.                                            Components.interfaces.nsISupportsString).toString();
  1401.             } catch (ex) {
  1402.                 LOG("error " + e + " fetching resource type");
  1403.             }
  1404.  
  1405.             if (resourceTypeXml.length == 0) {
  1406.                 resourceType = kDavResourceTypeNone;
  1407.             } else if (resourceTypeXml.indexOf("calendar") != -1) {
  1408.                 resourceType = kDavResourceTypeCalendar;
  1409.             } else if (resourceTypeXml.indexOf("collection") != -1) {
  1410.                 resourceType = kDavResourceTypeCollection;
  1411.             }
  1412.         }
  1413.  
  1414.         var calendarDirUri = this.mCalendarUri.clone();
  1415.         calendarDirUri.spec = this.makeUri('');
  1416.         var res = new WebDavResource(calendarDirUri);
  1417.         var webSvc = Components.classes['@mozilla.org/webdav/service;1'].
  1418.                      getService(Components.interfaces.nsIWebDAVService);
  1419.         try {
  1420.             webSvc.getResourceProperties(res, 1, ["DAV: resourcetype"], false,
  1421.                                           listener, this, null);
  1422.         } catch (ex) {
  1423.             thisCalendar.reportDavError(Components.interfaces.calIErrors.DAV_NO_PROPS,
  1424.                                         "dav_noProps");
  1425.         }
  1426.     },
  1427.  
  1428.     reportDavError: function caldav_rDE(aErrNo, aMessage) {
  1429.         this.onError(aErrNo, calGetString("calendar", aMessage, [this.mUri.spec]));
  1430.     },
  1431.  
  1432.     // stubs to keep callbacks we don't support yet from throwing errors 
  1433.     // we don't care about
  1434.     // nsIProgressEventSink
  1435.     onProgress: function onProgress(aRequest, aContext, aProgress, aProgressMax) {},
  1436.     onStatus: function onStatus(aRequest, aContext, aStatus, aStatusArg) {},
  1437.     // nsIDocShellTreeItem
  1438.     findItemWithName: function findItemWithName(name, aRequestor, aOriginalRequestor) {}
  1439. };
  1440.  
  1441. function WebDavResource(url) {
  1442.     this.mResourceURL = url;
  1443. }
  1444.  
  1445. WebDavResource.prototype = {
  1446.     mResourceURL: {},
  1447.     get resourceURL() { 
  1448.         return this.mResourceURL;}  ,
  1449.     QueryInterface: function(iid) {
  1450.         if (iid.equals(CI.nsIWebDAVResource) ||
  1451.             iid.equals(CI.nsISupports)) {
  1452.             return this;
  1453.         }
  1454.        
  1455.         throw Components.interfaces.NS_ERROR_NO_INTERFACE;
  1456.     }
  1457. };
  1458.  
  1459. function WebDavListener() {
  1460. }
  1461.  
  1462. WebDavListener.prototype = { 
  1463.  
  1464.     QueryInterface: function (aIID) {
  1465.         if (!aIID.equals(Components.interfaces.nsISupports) &&
  1466.             !aIID.equals(nsIWebDavOperationListener)) {
  1467.             throw Components.results.NS_ERROR_NO_INTERFACE;
  1468.         }
  1469.  
  1470.         return this;
  1471.     },
  1472.  
  1473.     onOperationComplete: function(aStatusCode, aResource, aOperation,
  1474.                                   aClosure) {
  1475.         // aClosure is the listener
  1476.         aClosure.onOperationComplete(this, aStatusCode, 0, null, null);
  1477.  
  1478.         LOG("WebDavListener.onOperationComplete() called");
  1479.         return;
  1480.     },
  1481.  
  1482.     onOperationDetail: function(aStatusCode, aResource, aOperation, aDetail,
  1483.                                 aClosure) {
  1484.         LOG("WebDavListener.onOperationDetail() called");
  1485.         return;
  1486.     }
  1487. }
  1488.  
  1489. /****
  1490.  **** module registration
  1491.  ****/
  1492.  
  1493. var calDavCalendarModule = {
  1494.     mCID: Components.ID("{a35fc6ea-3d92-11d9-89f9-00045ace3b8d}"),
  1495.     mContractID: "@mozilla.org/calendar/calendar;1?type=caldav",
  1496.  
  1497.     mUtilsLoaded: false,
  1498.     loadUtils: function caldavLoadUtils() {
  1499.         if (this.mUtilsLoaded)
  1500.             return;
  1501.  
  1502.         const jssslContractID = "@mozilla.org/moz/jssubscript-loader;1";
  1503.         const jssslIID = Components.interfaces.mozIJSSubScriptLoader;
  1504.  
  1505.         const iosvcContractID = "@mozilla.org/network/io-service;1";
  1506.         const iosvcIID = Components.interfaces.nsIIOService;
  1507.  
  1508.         var loader = Components.classes[jssslContractID].getService(jssslIID);
  1509.         var iosvc = Components.classes[iosvcContractID].getService(iosvcIID);
  1510.  
  1511.         // Note that unintuitively, __LOCATION__.parent == .
  1512.         // We expect to find utils in ./../js
  1513.         var appdir = __LOCATION__.parent.parent;
  1514.         appdir.append("js");
  1515.         var scriptName = "calUtils.js";
  1516.  
  1517.         var f = appdir.clone();
  1518.         f.append(scriptName);
  1519.  
  1520.         try {
  1521.             var fileurl = iosvc.newFileURI(f);
  1522.             loader.loadSubScript(fileurl.spec, this.__parent__.__parent__);
  1523.         } catch (e) {
  1524.             LOG("Error while loading " + fileurl.spec );
  1525.             throw e;
  1526.         }
  1527.  
  1528.         this.mUtilsLoaded = true;
  1529.     },
  1530.     
  1531.     registerSelf: function (compMgr, fileSpec, location, type) {
  1532.         compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
  1533.         compMgr.registerFactoryLocation(this.mCID,
  1534.                                         "Calendar CalDAV back-end",
  1535.                                         this.mContractID,
  1536.                                         fileSpec,
  1537.                                         location,
  1538.                                         type);
  1539.     },
  1540.  
  1541.     getClassObject: function (compMgr, cid, iid) {
  1542.         if (!cid.equals(this.mCID))
  1543.             throw Components.results.NS_ERROR_NO_INTERFACE;
  1544.  
  1545.         if (!iid.equals(Components.interfaces.nsIFactory))
  1546.             throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  1547.  
  1548.         this.loadUtils();
  1549.  
  1550.         return this.mFactory;
  1551.     },
  1552.  
  1553.     mFactory: {
  1554.         createInstance: function (outer, iid) {
  1555.             if (outer != null)
  1556.                 throw Components.results.NS_ERROR_NO_AGGREGATION;
  1557.             return (new calDavCalendar()).QueryInterface(iid);
  1558.         }
  1559.     },
  1560.  
  1561.     canUnload: function(compMgr) {
  1562.         return true;
  1563.     }
  1564. };
  1565.  
  1566.  
  1567. function NSGetModule(compMgr, fileSpec) {
  1568.     return calDavCalendarModule;
  1569. }
  1570.